using System;
using System.Linq;
using System.Collections;
using System.Web.UI;
using System.Linq.Expressions;
using System.Collections.Generic;
using System.Reflection;
using System.CodeDom;
using System.ComponentModel.Design;
using System.Text;
using System.CodeDom.Compiler;
using System.IO;

namespace MvcImproved.Generic
{
	class GenericControlBuilderHelper
	{
		private string LambdaMemberAccess;
		private string LambdaSubMemberAccess;
		private Type MemberInstanceType = null;
		private Type ParentModelType = null;
		private Type SubMemberInstanceType = null;
		static PropertyInfo InDesignerMember = typeof(ControlBuilder).GetProperty("InDesigner", BindingFlags.Instance | BindingFlags.NonPublic);
		static PropertyInfo FInDesignerParserMember = typeof(TemplateParser).GetProperty("FInDesigner", BindingFlags.Instance | BindingFlags.NonPublic);
		static PropertyInfo ParentBuilderMember = typeof(TemplateBuilder).GetProperty("ParentBuilder", BindingFlags.Instance | BindingFlags.NonPublic);
		static PropertyInfo BaseTypeMember = typeof(TemplateParser).GetProperty("BaseType", BindingFlags.Instance | BindingFlags.NonPublic);
		GenericControlTypeAttribute GenericData;
		bool InDesigner;
		ControlBuilder Builder;
		IServiceProvider ServiceProvider;

		public GenericControlBuilderHelper(bool inDesigner, ControlBuilder builder, IServiceProvider serviceProvider)
		{
			InDesigner = inDesigner;
			Builder = builder;
			ServiceProvider = serviceProvider;
		}

		private static bool MatchesGenericInterface(Type queryType, params Type[] interfaceType)
		{
			Type genericType = queryType.GetGenericTypeDefinition();
			foreach (Type t in interfaceType)
			{
				if (genericType == t)
					return true;
			}

			return false;
		}

		public Type Init(TemplateParser parser, ControlBuilder parentBuilder, Type type,
			string tagName, string id, IDictionary attribs)
		{
			//In designer mode (which is sometimes signaled with a null parentBuilder), we simply default to the non-templatized member 
			if (this.InDesigner || ((parser != null) && (bool)FInDesignerParserMember.GetValue(parser, null))
				|| ((parentBuilder != null) && (bool)InDesignerMember.GetValue(parentBuilder, null)))
			{
				return type;
			}

			GenericControlTypeAttribute data = type.GetCustomAttributes(typeof(GenericControlTypeAttribute), true).Cast<GenericControlTypeAttribute>().FirstOrDefault();
			if (data == null)
				throw new InvalidOperationException(String.Format("GenericControlBuilder requires a GenericControlType attribute, but was not found in {0}", type));

			GenericData = data;

			List<Type> genericArgList;
			string rawLambdaMemberAccess = data.GetReferencedLambda(attribs);
			if (attribs.Contains(data.GenericParameters))
			{
				// If objecttype is specified, create a generic type that is bound to that
				// argument and then hide the objecttype attribute.
				genericArgList = data.GenericArgumentList(attribs, InDesigner, ServiceProvider);

				attribs.Remove(data.GenericParameters);
			}
			else //Otherwise, try to get the type from the model type 
			{
				genericArgList = new List<Type>();

				bool useRoot;
				Type genericView = GetParentGenericView(parentBuilder, rawLambdaMemberAccess, out useRoot);

				if (((genericView == null) || useRoot) && (parser != null))
				{
					Type parentTypeCheck = (Type)BaseTypeMember.GetValue(parser, null);
					while (parentTypeCheck != null)
					{
						Type parentGeneric = parentTypeCheck.IsGenericType ? parentTypeCheck.GetGenericTypeDefinition() : null;
						if (parentTypeCheck.IsGenericType && ((typeof(System.Web.Mvc.ViewPage<>) == parentGeneric) || (typeof(System.Web.Mvc.ViewUserControl<>) == parentGeneric)))
							break;
						parentTypeCheck = parentTypeCheck.BaseType;
					}

					genericView = parentTypeCheck;
				}

				if (genericView == null)
					throw new InvalidOperationException(String.Format("Generic control {0} does not specify Model, and parent page {0} is not a {1} or {2}. Are you using a code behind and missing a CodeFileBaseClass attribute in your Page directive?", id, parentBuilder.NamingContainerType, typeof(System.Web.Mvc.ViewPage<>), typeof(System.Web.Mvc.ViewUserControl<>)));
				else
					genericArgList.Add(genericView.GetGenericArguments()[0]);
			}

			ParentModelType = genericArgList[genericArgList.Count - 1];
			LambdaMemberAccess = (rawLambdaMemberAccess != null) ? rawLambdaMemberAccess.TrimStart(new char[] { '.', '\\' }) : null;
			Type memberArg = GetAccessedType(LambdaMemberAccess, ParentModelType);
			if (genericArgList.Count < data.GenericControl.GetGenericArguments().Length)
			{
				// The generic control takes in a generic argument for the field type.
				genericArgList.Add(memberArg);
			} // Otherwise, the generic control may simply only deal with a certain type of member

			//Check if type is an IEnumerable, and if it is, pass the type of the element as a generic parameter
			Type instanceType = null;
			if (genericArgList.Count < data.GenericControl.GetGenericArguments().Length)
			{
				Type genericInterface;
				if (memberArg.IsGenericType && (memberArg.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
					genericInterface = memberArg;
				else
					genericInterface = (from i in memberArg.GetInterfaces() where i.IsGenericType && MatchesGenericInterface(i, typeof(IEnumerable<>)) select i).FirstOrDefault();

				if (genericInterface != null)
					instanceType = genericInterface.GetGenericArguments()[0];
			}

			if (instanceType != null)
				genericArgList.Add(instanceType);
			
			MemberInstanceType = memberArg;

			if (!String.IsNullOrEmpty(data.SubMemberValue))
			{
				var rawSubLambda = data.GetReferencedSubMemberLambda(attribs);

				Type relativeModel;
				if ((rawSubLambda != null) && rawSubLambda.StartsWith("."))
				{
					// "Root" for a sub-member has a different meaning - it means we use the same model as the lambda member access
					LambdaSubMemberAccess = rawSubLambda.Substring(1);
					relativeModel = ParentModelType;
				}
				else if ((rawSubLambda != null) && rawSubLambda.StartsWith("\\"))
				{
					// "Parent" for a sub-member has a different meaning - it means to use the member itself
					LambdaSubMemberAccess = rawSubLambda.Substring(1);
					relativeModel = memberArg;
				}
				else
				{
					LambdaSubMemberAccess = rawSubLambda;
					relativeModel = (instanceType != null) ? instanceType : memberArg;
				}
				if ((LambdaSubMemberAccess != null) && LambdaSubMemberAccess.TrimStart(new char[] { '.', '\\' }) != LambdaSubMemberAccess)
					throw new InvalidOperationException("Unexpected root (.) or parent (\\) accessor in SubMember (only 1 level allowed)");

				SubMemberInstanceType = GetAccessedType(LambdaSubMemberAccess, relativeModel);

				if (genericArgList.Count < data.GenericControl.GetGenericArguments().Length)
					genericArgList.Add(SubMemberInstanceType);
			}

			Type concreteType = data.GenericControl.MakeGenericType(genericArgList.ToArray());

			return new GenericControlTypeDelegator(concreteType);
		}

		private Type GetParentGenericView(ControlBuilder parentCheck, string rawLambdaMemberAccess, out bool useRoot)
		{
			Type genericView = null;
			var upChain = new Stack<char>();
			useRoot = false;

			if (rawLambdaMemberAccess != null)
			{
				// Are we referencing the root?
				useRoot = rawLambdaMemberAccess.StartsWith(".");

				// Are we referencing a parent container model?
				foreach (char chainCheck in rawLambdaMemberAccess)
				{
					if (chainCheck != '\\')
						break;

					upChain.Push(chainCheck);
				}
			}

			while (parentCheck != null)
			{
				Type declaringType = parentCheck.ControlType;
				if (parentCheck is TemplateBuilder)
				{
					try
					{
						ControlBuilder builder = (ControlBuilder)ParentBuilderMember.GetValue(parentCheck, null);
						Type grandparentType = null;
						if (builder != null)
							grandparentType = builder.ControlType.UnderlyingSystemType;
						if (grandparentType != null)
						{
							PropertyInfo parentProperty = (new GenericControlTypeDelegator(grandparentType)).GetProperty(parentCheck.TagName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
							if (parentProperty != null)
							{
								TemplateContainerAttribute genericTemplateAttribute = parentProperty.GetCustomAttributes(typeof(TemplateContainerAttribute), true).Cast<TemplateContainerAttribute>().SingleOrDefault();
								if (genericTemplateAttribute != null)
								{
									declaringType = genericTemplateAttribute.ContainerType;
								}
							}
						}
					}
					catch (Exception)
					{ }
				}

				var nextGenericView = (from i in declaringType.GetInterfaces() where i.IsGenericType && MatchesGenericInterface(i, typeof(IViewDataContainer<>)) select i).FirstOrDefault();
				if (nextGenericView != null)
				{
					genericView = nextGenericView;
					if (!useRoot)
						return genericView;
				}

				parentCheck = (ControlBuilder)ParentBuilderMember.GetValue(parentCheck, null);
			}

			return genericView;
		}

		protected string GetTypeString(Type type)
		{
			var codeProvider = new Microsoft.CSharp.CSharpCodeProvider();

			StringBuilder sb = new StringBuilder();
			TextWriter tWriter = new IndentedTextWriter(new StringWriter(sb));
			codeProvider.GenerateCodeFromExpression(new CodeTypeReferenceExpression(type), tWriter, new CodeGeneratorOptions());
			codeProvider.Dispose();

			tWriter.Close();

			return sb.ToString();
		}

		private CodeSnippetExpression GenerateLambda(bool useHelper, Type modelType, string memberAccess)
		{
			string lambdaExpression;
			string modelTypeString = GetTypeString(modelType);
			string parentTypeString = GetTypeString(ParentModelType);
			if (String.IsNullOrEmpty(memberAccess))
				lambdaExpression = "ma => ma";
			else
			{
				if (memberAccess[0] == '[') //Indexer access
					lambdaExpression = String.Format("ma => (ma == null) ? default({0}) : ma{1}", modelTypeString, memberAccess);
				else
					lambdaExpression = String.Format("ma => (ma == null) ? default({0}) : ma.{1}", modelTypeString, memberAccess);
			}

			if (useHelper)
				return new CodeSnippetExpression(String.Format("MvcImproved.Generic.IControlModelHelper.Get<{0}, {1}>({2})", parentTypeString, modelTypeString, lambdaExpression));
			else
				return new CodeSnippetExpression(lambdaExpression);
		}

		protected CodeSnippetExpression GenerateLambda(bool useHelper)
		{
			return GenerateLambda(useHelper, MemberInstanceType, LambdaMemberAccess);
		}

		protected CodeSnippetExpression GenerateSubLambda(bool useHelper)
		{
			if (!String.IsNullOrEmpty(LambdaSubMemberAccess) && (SubMemberInstanceType != null))
				return GenerateLambda(useHelper, SubMemberInstanceType, LambdaSubMemberAccess);
			else
				return null;
		}

		private Type GetAccessedType(string lambdaMemberAccess, Type parent)
		{
			if (lambdaMemberAccess == null)
				return parent;

			Type memberType = parent;

			string[] subMembers = lambdaMemberAccess.Split('.');
			foreach (string m in subMembers)
			{
				Type subMemberType;
				string memberName = m;
				int arrayAccess = m.IndexOf('[');
				if (arrayAccess != -1)
					memberName = memberName.Substring(0, arrayAccess);

				if (String.IsNullOrEmpty(memberName))
				{
					if (arrayAccess != -1)
						subMemberType = memberType;
					else
						throw new InvalidOperationException(String.Format("Empty string specified for field or property for {0}", memberType));
				}
				else
				{
					PropertyInfo property = memberType.GetProperty(memberName);
					if (property != null)
						subMemberType = property.PropertyType;
					else
					{
						FieldInfo field = memberType.GetField(memberName);
						if (field == null)
							subMemberType = null;
						else
							subMemberType = field.FieldType;
					}
				}

				//Process array access
				if ((subMemberType != null) && (arrayAccess != -1))
				{
					if (subMemberType.IsArray)
						subMemberType = subMemberType.GetElementType();
					else
						subMemberType = (from d in subMemberType.GetDefaultMembers() where (d.MemberType == MemberTypes.Property) && ((PropertyInfo)d).GetIndexParameters().Length > 0 select ((PropertyInfo)d).PropertyType).FirstOrDefault();
				}

				if (subMemberType == null)
					throw new InvalidOperationException(String.Format("Type {0} does not contain a property or field named {1}", memberType, memberName));
				memberType = subMemberType;
			}

			return memberType;
		}
	}

	public class GenericControlBuilder : ControlBuilder
	{
		private class BuilderHelper : GenericControlBuilderHelper
		{
			public BuilderHelper(bool inDesigner, ControlBuilder builder, IServiceProvider serviceProvider)
				: base(inDesigner, builder, serviceProvider)
			{ }

			public void ProcessGeneratedCode(CodeCompileUnit codeCompileUnit, CodeTypeDeclaration baseType, CodeTypeDeclaration derivedType, CodeMemberMethod buildMethod, CodeMemberMethod dataBindingMethod)
			{
				//In DesignMode, we have no buildMethod
				if (buildMethod != null)
				{
					foreach (CodeStatement statement in buildMethod.Statements)
					{
						CodeAssignStatement assignment = statement as CodeAssignStatement;
						if ((assignment != null) && (assignment.Left is CodeVariableReferenceExpression) && (assignment.Right is CodeObjectCreateExpression))
						{
							CodeObjectCreateExpression right = assignment.Right as CodeObjectCreateExpression;
							right.Parameters.Clear();

							//TODO: Replace this with GenerateLambda(true).  Will require a change to all controls
							//to take a ControlModelHelper instead of a 2-var lambda
							right.Parameters.Add(GenerateLambda(false));

							//TODO: Replace this with GenerateSubLambda(true), see above
							var subMemberLambda = GenerateSubLambda(false);
							if (subMemberLambda != null)
								right.Parameters.Add(subMemberLambda);
						}
					}
				}
			}
		}

		private BuilderHelper _BuilderHelper;
		public override void Init(TemplateParser parser, ControlBuilder parentBuilder, Type type,
			string tagName, string id, IDictionary attribs)
		{
			_BuilderHelper = new BuilderHelper(InDesigner, this, ServiceProvider);

			type = _BuilderHelper.Init(parser, parentBuilder, type, tagName, id, attribs);

			base.Init(parser, parentBuilder, type, tagName, id, attribs);
		}

		public override void ProcessGeneratedCode(CodeCompileUnit codeCompileUnit, CodeTypeDeclaration baseType, CodeTypeDeclaration derivedType, CodeMemberMethod buildMethod, CodeMemberMethod dataBindingMethod)
		{
			base.ProcessGeneratedCode(codeCompileUnit, baseType, derivedType, buildMethod, dataBindingMethod);

			_BuilderHelper.ProcessGeneratedCode(codeCompileUnit, baseType, derivedType, buildMethod, dataBindingMethod);
		}
	}

	public class GenericUserControlInstanceBuilder : UserControlControlBuilder
	{
		private class BuilderHelper : GenericControlBuilderHelper
		{
			public BuilderHelper(bool inDesigner, ControlBuilder builder, IServiceProvider serviceProvider)
				: base(inDesigner, builder, serviceProvider)
			{ }

			public void ProcessGeneratedCode(CodeCompileUnit codeCompileUnit, CodeTypeDeclaration baseType, CodeTypeDeclaration derivedType, CodeMemberMethod buildMethod, CodeMemberMethod dataBindingMethod)
			{
				//In DesignMode, we have no buildMethod
				if (buildMethod != null)
				{
					CodeVariableReferenceExpression controlReference = null;
					int idx = 0;
					foreach (CodeStatement statement in buildMethod.Statements)
					{
						CodeAssignStatement assignment = statement as CodeAssignStatement;
						if ((assignment != null) && (assignment.Left is CodeVariableReferenceExpression) && (assignment.Right is CodeObjectCreateExpression))
						{
							controlReference = (CodeVariableReferenceExpression)assignment.Left;
							break;
						}
						idx++;
					}

					if (controlReference != null)
					{
						var parameters = new List<CodeExpression>();
						parameters.Add(GenerateLambda(true));

						var subMemberLambda = GenerateSubLambda(true);
						if (subMemberLambda != null)
							parameters.Add(subMemberLambda);

						CodeExpression invokeExpression = new CodeMethodInvokeExpression(controlReference, "InitValueMember", parameters.ToArray());
						buildMethod.Statements.Insert(idx + 1, new CodeExpressionStatement( invokeExpression ));
					}
				}
			}
		}

		private BuilderHelper _BuilderHelper;
		public override void Init(TemplateParser parser, ControlBuilder parentBuilder, Type type,
			string tagName, string id, IDictionary attribs)
		{
			_BuilderHelper = new BuilderHelper(InDesigner, this, ServiceProvider);

			_BuilderHelper.Init(parser, parentBuilder, type, tagName, id, attribs);

			// We don't pass the processed type here because the page-level parser will already have set
			// the correct generic base
			base.Init(parser, parentBuilder, type, tagName, id, attribs);
		}

		public override void ProcessGeneratedCode(CodeCompileUnit codeCompileUnit, CodeTypeDeclaration baseType, CodeTypeDeclaration derivedType, CodeMemberMethod buildMethod, CodeMemberMethod dataBindingMethod)
		{
			base.ProcessGeneratedCode(codeCompileUnit, baseType, derivedType, buildMethod, dataBindingMethod);

			_BuilderHelper.ProcessGeneratedCode(codeCompileUnit, baseType, derivedType, buildMethod, dataBindingMethod);
		}
	}
}
